/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package cz.mzk.oai.metadataRepository.repository;

import cz.mzk.kramerius.commons.PropertyLoader;
import cz.mzk.oai.metadataRepository.metadata.DateStamp;
import cz.mzk.oai.metadataRepository.metadata.MetadataFormat;
import cz.mzk.oai.metadataRepository.repository.collections.MetadataCollection;
import cz.mzk.oai.metadataRepository.repository.collections.NoCollectionFoundException;
import cz.mzk.oai.metadataRepository.repository.records.NoSuchRecordException;
import cz.mzk.oai.metadataRepository.repository.records.Record;
import cz.mzk.oai.metadataRepository.repository.records.inMemory.RecordInMemory;
import cz.mzk.oai.metadataRepository.repository.records.presentAndDeleted.PresentRecord;
import cz.mzk.oai.metadataRepository.repository.xmlConfiguration.ConfigurationLoader;
import cz.mzk.oai.metadataRepository.tools.CannotReadDirectoryException;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.dom4j.Document;
import org.dom4j.DocumentException;

/**
 * @author Martin Řehánek (rehan at mzk.cz) The repository cannot detect that
 * there has been some change in collections if this change is performad by
 * something external. For example if you run fileHarvester than you must
 * restart web application in order for it to realize the changes
 *
 */
public class Repository {

    private static final char eoln = '\n';
    private static final Logger logger = Logger.getLogger(Repository.class.getName());
    private static final String PROP_CONFIG_LOADER_XML = "repositoryManager.Repository.config";
    private static Repository instance;
    private List<MetadataCollection> collections;
    private Set<OaiSet> sets;

    public static Repository instanceOf(Properties properties) {
        if (instance == null) {
            loadRepository(properties);
        }
        return instance;
    }

    private static void loadRepository(Properties properties) {
        try {
            String propertyName = "javax.xml.transform.TransformerFactory";
            String propertyValue = "net.sf.saxon.TransformerFactoryImpl";
            System.setProperty(propertyName, propertyValue);
            logger.log(Level.INFO, "Setting system property {0} to \"{1}\"", new Object[]{propertyName, propertyValue});
            instance = new Repository(properties);
        } catch (IOException ex) {
            logger.log(Level.SEVERE, null, ex);
        } catch (DocumentException ex) {
            logger.log(Level.SEVERE, null, ex);
        }
    }

    /**
     * @param properties properties
     * @throws IOException if failed to initialize some configuration file,
     * repository directory or xslt document
     */
    private Repository(Properties properties) throws IOException, DocumentException {
        loadCollectionsAndSets(properties);
    }

    private void loadCollectionsAndSets(Properties properties) throws IOException, DocumentException {
        ConfigurationLoader configLoader = initConfigurationLoader(properties);
        collections = new ArrayList<MetadataCollection>(configLoader.getCollections());
        sets = configLoader.getSets();
    }

    private ConfigurationLoader initConfigurationLoader(Properties properties) throws IOException, DocumentException {
        PropertyLoader propertyLoader = new PropertyLoader(properties);
        File collectionAndSetsConfigurationXml = propertyLoader.loadFile(PROP_CONFIG_LOADER_XML, true, true, false);
        System.err.println("config: " + collectionAndSetsConfigurationXml.getAbsolutePath());
        return new ConfigurationLoader(collectionAndSetsConfigurationXml);
    }

    /**
     *
     * @param name
     * @return Collection with name 'name' if it exists, null otherwise
     */
    public MetadataCollection findCollection(String name) {
        for (MetadataCollection col : collections) {
            if (name.equals(col.getName())) {
                return col;
            }
        }
        return null;
    }

    public List<MetadataCollection> getCollections() {
        return collections;
    }

    public Set<OaiSet> getSets() {
        return sets;
    }

    public void updateRecord(Identifier id, MetadataFormat format, Document metadata) throws IOException, NoCollectionFoundException {
        MetadataCollection collection = selectCollection(id);
        collection.addOrUpdateRecord(id, format, metadata);
    }

    public void setRecordDeleted(Identifier id, MetadataFormat format) throws NoSuchRecordException, IOException {
        MetadataCollection collection = findCollectionThatContainsItem(id);
        if (collection == null) {
            throw new NoSuchRecordException(id, format);
        }
        collection.setDeleted(id, format);
        collection.setDerivedRecorsDeleted(id, format);
    }

    public void updateDerivedRecords(MetadataCollection collection, MetadataFormat primaryFormat) {
        int counter = 0;
        for (Record record : collection.getRecords(primaryFormat, null, null)) {
            if (!record.isDeleted()) {
                System.out.println("record #" + counter++ + ", id: " + record.getId());
                collection.updateDerivedRecords((PresentRecord) record);
                System.out.println("-------------");
            }
        }
    }

    /**
     *
     * @param id
     * @return Collection that contains item with identifier id
     * @throws NoCollectionFoundException if no such collection is found (and
     * therefore item is not present in repository)
     */
    public MetadataCollection selectCollection(Identifier id) throws NoCollectionFoundException {
        MetadataCollection collectionFound = findCollectionThatContainsItem(id);
        if (collectionFound == null) {
            throw new NoCollectionFoundException();
        }
        return collectionFound;
    }

    private MetadataCollection findCollectionThatContainsItem(Identifier id) {
        for (MetadataCollection collection : collections) {
            if (collection.containsItem(id)) {
                return collection;
            }
        }
        return null;
    }

    public void moveItemBetweenCollections(Identifier id, MetadataCollection from, MetadataCollection to) {
        //todo: this should be transactional.
        //but i don't know how to achieve it (without excessive effort)
        //for now will just log if something in 'transaction' failes
        boolean loaded = false;
        boolean removed = false;
        boolean added = false;
        try {
            List<RecordInMemory> records = from.toRecordsInMemory(id);
            loaded = true;
            from.removeAllRecords(id);
            removed = true;
            to.addRecordsFromMemory(records);
            added = true;
        } catch (Exception e) {
            if (!loaded) {
                logger.log(Level.SEVERE, "''{0}'': failed to load some record: {1}", new Object[]{id.toString(), e.getMessage()});
            } else if (loaded && !removed) {
                logger.log(Level.SEVERE, "''{0}'': error removing record. Some records might be permanently removed, others not: {1}", new Object[]{id.toString(), e.getMessage()});
            } else if (loaded && removed && !added) {
                logger.log(Level.SEVERE, "''{0}'': error adding record. Some records might be added, others not (but they were still removed! from original collection): {1}", new Object[]{id.toString(), e.getMessage()});
            }
        }
    }

    private List<OaiSet> findSets(String setSpec) {
        OaiSet byName = oaiSetByName(setSpec);
        if (byName != null) {
            List<OaiSet> result = new ArrayList<OaiSet>();
            result.add(byName);
            return result;
        } else {
            return oaiSetsByPrefix(setSpec.concat(":"));
        }
    }

    private OaiSet oaiSetByName(String name) {
        for (OaiSet set : sets) {
            if (set.getSetSpec().equals(name)) {
                return set;
            }
        }
        return null;
    }

    private List<OaiSet> oaiSetsByPrefix(String prefix) {
        List<OaiSet> result = new ArrayList<OaiSet>();
        for (OaiSet set : sets) {
            if (set.getSetSpec().startsWith(prefix)) {
                result.add(set);
            }
        }
        return result;
    }

    public Record getRecord(Identifier id, MetadataFormat format, boolean validate) throws IOException {
        for (MetadataCollection col : collections) {
            if (col.containsRecord(id, format)) {
                //System.err.println("col:" + col.getName() + " contains item id: " + id.toString());
                return col.getRecord(id, format, validate);
            }
        }
        return null;
    }

    public Set<Record> getRecords(MetadataFormat metadataFormat, DateStamp from, DateStamp until) {
        Set<Record> result = new HashSet<Record>();
        for (MetadataCollection collection : collections) {
            List<Record> recordsInCollection = collection.getRecords(metadataFormat, from, until);
            result.addAll(recordsInCollection);
        }
        return result;
    }

    public Set<Record> getRecords(MetadataFormat metadataFormat, String setSpec, DateStamp from, DateStamp until) {
        List<OaiSet> oaiSets = findSets(setSpec);
        if (oaiSets.isEmpty()) {
            return Collections.<Record>emptySet();
        } else {
            Set<Record> result = new HashSet<Record>();
            List<MetadataCollection> collectionList = collectionsInSets(oaiSets);
            for (MetadataCollection collection : collectionList) {
                List<Record> recordsInCollection = collection.getRecords(metadataFormat, from, until);
                result.addAll(recordsInCollection);
            }
            return result;
        }
    }

    private List<MetadataCollection> collectionsInSets(List<OaiSet> sets) {
        List<MetadataCollection> result = new ArrayList<MetadataCollection>();
        for (OaiSet set : sets) {
            List<MetadataCollection> collectionsInSet = collectionsInSet(set);
            addIfNotPresent(result, collectionsInSet);
        }
        return result;
    }

    private void addIfNotPresent(List<MetadataCollection> base, List<MetadataCollection> toAdd) {
        for (MetadataCollection collection : toAdd) {
            if (!base.contains(collection)) {
                base.add(collection);
            }
        }
    }

    private List<MetadataCollection> collectionsInSet(OaiSet set) {
        List<MetadataCollection> result = new ArrayList<MetadataCollection>();
        for (MetadataCollection collection : collections) {
            if (collection.isInOaiSet(set)) {
                result.add(collection);
            }
        }
        return result;
    }

    public void printSets() {
        System.out.println("Repository.printSets(): Not implemented yet");
    }

    /**
     * Iterates through all collections and prints number of invalid files in
     * format
     *
     * @param format
     */
    public void validateMetadata(MetadataFormat format, boolean printInvalidFiles) {
        int total = 0;
        int notValid = 0;
        for (MetadataCollection col : collections) {
            if (col.supportsFormat(format)) {
                logger.log(Level.INFO, "collection: ''{0}''", col.getName());
                File dir = new File(col.getRootDir().getAbsolutePath() + File.separator + format.getDirName());
                try {
                    MetadataValidator validator = new MetadataValidator(dir, format);
                    validator.validate(printInvalidFiles);
                    total += validator.getAllFiles().size();
                    notValid += validator.getInvalidFiles().size();
                } catch (CannotReadDirectoryException ex) {
                    Logger.getLogger(Repository.class.getName()).log(Level.SEVERE, ex.getMessage());
                }
                logger.log(Level.INFO, "------------------------------");
            }
        }
        System.out.println("total: " + total);
        System.out.println("not valid: " + notValid);
    }

    /**
     * prints number of invalid files in format
     *
     * @param format
     * @param collectionName
     * @param prinInvalidFiles
     */
    public void validateMetadata(MetadataFormat format, String collectionName, boolean printInvalidFiles) {
        MetadataCollection collection = findCollection(collectionName);
        if (collection == null) {
            Logger.getLogger(Repository.class.getName()).log(Level.SEVERE, "No such a collection ''{0}''", collectionName);
        } else {
            if (collection.supportsFormat(format)) {
                logger.log(Level.INFO, "collection: ''{0}''", collectionName);
                File dir = new File(collection.getRootDir().getAbsolutePath() + File.separator + format.getDirName());
                try {
                    MetadataValidator validator = new MetadataValidator(dir, format);
                    validator.validate(printInvalidFiles);
                    System.out.println("total: " + validator.getAllFiles().size());
                    System.out.println("not valid: " + validator.getInvalidFiles().size());
                } catch (CannotReadDirectoryException ex) {
                    Logger.getLogger(Repository.class.getName()).log(Level.SEVERE, ex.getMessage());
                }
            } else {
                logger.log(Level.SEVERE, "collection ''{0}'' is not in format ''{1}''", new Object[]{collectionName, format});
            }
        }
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append("Collections :").append(eoln).append(eoln);
        Map<MetadataFormat, Integer> counterMap = new HashMap<MetadataFormat, Integer>();
        int itemCounter = 0;
        for (MetadataCollection col : collections) {
            builder.append(col.toString());
        }
        builder.append("All items: ").append(itemCounter).append(eoln);
        for (MetadataFormat format : MetadataFormat.values()) {
            builder.append(format.toString()).append(": ");
            builder.append(counterMap.get(format)).append(eoln);
        }
        return builder.toString();
    }

    private void printCollections() {
        for (MetadataCollection col : collections) {
            System.out.println("collection: " + col.getName() + ", sets:");
            for (OaiSet set : col.getOaiSets()) {
                System.out.println(set.getSetSpec());
            }
            System.out.println("");
        }
    }
}
